/*
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
 */
package erwiki.part.providers;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Platform;
import org.eclipse.emf.cdo.transaction.CDOTransaction;
import org.eclipse.emf.cdo.view.CDOQuery;
import org.eclipse.emf.cdo.view.CDOView;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EClass;
import org.elwiki.IWikiConstants;
import org.elwiki_data.AttachmentContent;
import org.elwiki_data.Elwiki_dataPackage;
import org.elwiki_data.PageAttachment;
import org.elwiki_data.WikiPage;

import erwiki.api.IStorageCdo;
import erwiki.api.WikiPartReference;
import erwiki.api.component.IPartPreferences;
import erwiki.api.core.Engine;
import erwiki.api.exceptions.NoRequiredPropertyException;
import erwiki.api.exceptions.ProviderException;
import erwiki.api.exceptions.WikiException;
import erwiki.api.pages.PageManager;
import erwiki.api.providers.AttachmentProvider;
import erwiki.util.FileUtil;

/**
 * Provides basic, versioning attachments.</br>
 * This class converted from version of JSPwiki - porting code based on attachment items of EMF
 * model.</br>
 * Attachment files are stored in the location of attachment area (with random names). Metadata of
 * attachment items are stored in the CDO repository, and are linked with corresponded wiki page.
 *
 * Old format:
 *
 * <PRE>
 *   Structure is as follows:
 *      attachment_dir/
 *         ThisPage/
 *            attachment.doc/
 *               attachment.properties
 *               1.doc
 *               2.doc
 *               3.doc
 *            picture.png/
 *               attachment.properties
 *               1.png
 *               2.png
 *         ThatPage/
 *            picture.png/
 *               attachment.properties
 *               1.png
 *
 * </PRE>
 */
public class BasicAttachmentProvider implements AttachmentProvider, IPartPreferences {

	private static final Logger log = Logger.getLogger(BasicAttachmentProvider.class);

	interface Prefs {
		/** Имя свойства, указывающее каталог, используемый для хранения вложений. */
		String STORAGE_DIR = "basicAttachmentProvider.storageDir";

		/** Имя свойства, указывающее, какие вложения не кэшируются. */
		String DISABLE_CACHE = "basicAttachmentProvider.disableCache";
	}

	private static final String ATTACHMENTS_SUBDIRECTORY = "pages_attachment";

	private static final String ATTFILE_PREFIX = "att-";

	private static final String ATTFILE_SUFFIX = ".dat";

	/** The name of the property file. */
	public static final String PROPERTY_FILE = "attachment.properties";

	/** The default extension for the page attachment directory name. */
	public static final String DIR_EXTENSION = "-att";

	/** The default extension for the attachment directory. */
	public static final String ATTDIR_EXTENSION = "-dir";

	@WikiPartReference
	private Engine m_engine;

	@WikiPartReference
	private IStorageCdo storageCdo;

	/** Путь к месту размещения файлов вложений в локальной файловой системе. */
	private IPath attachmentStorePath;

	/** {@inheritDoc} */
	@Override
	public void initialize(Engine engine) throws NoRequiredPropertyException, IOException {
		resolveStoragePath();
	}

	/**
	 * Вычисляет расположение и проверяет существование каталога файлов вложений.
	 */
	private void resolveStoragePath() {
		IPath resultPath;
		if ((resultPath = FileUtil.getDirectoryPlace(BasicAttachmentProvider.Prefs.STORAGE_DIR)) == null) {
			String path = propsStorageDirectory();
			if (StringUtils.isBlank(path)) {
				// Каталог вложений ErWiki по умолчанию - в рабочей области Workbench.
				resultPath = m_engine.getWorkDir().append(ATTACHMENTS_SUBDIRECTORY);
			} else {
				// Путь к каталогу определить из заданного в конфигурации:
				// если задан относительный путь - то место расположения взять относительно рабочего каталога.
				resultPath = (Platform.OS.isWindows()) ? IPath.forWindows(path) : IPath.forPosix(path);
				if (!resultPath.isAbsolute()) {
					// В preferences задан относительный путь - вычислить относительно Work Directory.
					resultPath = m_engine.getWorkDir().append(resultPath);
				}
			}

			// Создать подкаталог вложений, если его нет.
			File dir = resultPath.toFile();
			try {
				if (!dir.exists()) {
					dir.mkdirs();
				}
				if (!dir.exists() || !dir.canWrite() || !dir.canRead()) {
					throw new WikiException("Cannot write to attachment directory: " + dir.getAbsolutePath());
				}
			} catch (Exception e) {
				log.error("Fail of create attachment directory: " + e.getMessage());
			}
		}

		this.attachmentStorePath = resultPath;

		log.info(" Attachment directory is: '" + this.attachmentStorePath + "'");
	}

	public String propsStorageDirectory() {
		return getPreference(BasicAttachmentProvider.Prefs.STORAGE_DIR, String.class);
	}

	public String propsDisabledCachingOf() {
		return getPreference(BasicAttachmentProvider.Prefs.DISABLE_CACHE, String.class);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public String getProviderInfo() {
		return "Provider: " + this.getClass().getName();
	}

	/**
	 * Finds the dir in which the attachment lives.
	 * <p>
	 * TODO: :FVK: The implementation should be extended to optimize directories containing attachments
	 * data files. A tree is planned in the file system to minimize the number of files per directory
	 * containing attachments
	 */
	@Deprecated
	private File findAttachmentDir(final AttachmentContent att) throws ProviderException {
		// :FVK: workaround. (here shoul added algotithm with hierarhy of directories, for controling
		// maximuum count of files in the one directory.)
		File f = this.attachmentStorePath.toFile();

		return f;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void putAttachmentData(WikiPage wikiPage, AttachmentContent attContent, String attName, InputStream data)
			throws ProviderException {
		File attDir = findAttachmentDir(null);

		if (!attDir.exists()) {
			attDir.mkdirs(); // workaround.
		}

		File newfile;
		try (OutputStream out = new FileOutputStream(
				newfile = File.createTempFile(ATTFILE_PREFIX, ATTFILE_SUFFIX, attDir))) {
			attContent.setPlace(newfile.getName());
			log.info("Uploading attachment " + attName + " to page " + wikiPage.getName());
			log.info("Saving attachment contents to " + newfile.getAbsolutePath());
			FileUtil.copyContents(data, out);

			PageManager pm = m_engine.getManager(PageManager.class);
			pm.addAttachment(wikiPage, attContent, attName);
		} catch (final Exception ex) {
			log.error("Could not save attachment data: ", ex);
			throw new ProviderException(ex);
		}
	}

	/** {@inheritDoc} */
	@Override
	public FileInputStream getAttachmentData(AttachmentContent atchContent) throws IOException, ProviderException {
		try {
			File f = this.attachmentStorePath.append(atchContent.getPlace()).toFile();
			return new FileInputStream(f);
		} catch (final FileNotFoundException e) {
			log.error("File not found: " + e.getMessage());
			throw new ProviderException("No such attachment was found.");
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public List<PageAttachment> listAttachments(final WikiPage page) throws ProviderException {
		log.debug("Listing attachments for page: " + page);
		List<PageAttachment> result = new ArrayList<>();

		for (PageAttachment pageAttachment : page.getAttachments()) {
			pageAttachment.forLastContent();
			result.add(pageAttachment);
		}

		return result;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public List<PageAttachment> listAllChanged(final Date timestamp) throws ProviderException {
		/* Now - nothing to do.
		 * FIXME: :FVK: Add into CDO manager - finding page attachment relate to given date.
		 */

		final ArrayList<PageAttachment> list = new ArrayList<>();
		// :FVK: list.sort( new PageTimeComparator() );

		return list;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public AttachmentContent getAttachmentContentById(String atchId, int... version) throws ProviderException {
		if (StringUtils.isBlank(atchId)) {
			throw new ProviderException("Wrong argument. Attachment ID is:'" + atchId + "'");
		}

		CDOTransaction transaction = null;
		try {
			transaction = storageCdo.getTransactionCDO();
			CDOQuery query;
			EClass eClassWikiPage = Elwiki_dataPackage.eINSTANCE.getWikiPage();
			query = transaction.createQuery("ocl",
					"PageAttachment.allInstances()->select(p:PageAttachment|p.id='" + atchId + "')", eClassWikiPage,
					false);
			query.setParameter("cdoLazyExtents", Boolean.FALSE);

			PageAttachment pageAttachment = null;
			List<PageAttachment> pages = query.getResult();
			if (pages.size() > 0) {
				pageAttachment = pages.get(0);
				CDOView view = storageCdo.getView();
				pageAttachment = (PageAttachment) view.getObject(pageAttachment.cdoID());
			}

			if (pageAttachment == null) {
				log.debug("Attachment ID=" + atchId + " not found - thus no attachment can exist.");
				throw new ProviderException("Attachment ID='" + atchId + "' not found.");
			}

			AttachmentContent attachmentContent;
			if (version.length == 0 || version[0] == IWikiConstants.LATEST_VERSION) {
				attachmentContent = pageAttachment.forLastContent();
			} else {
				attachmentContent = pageAttachment.forVersionContent(version[0]);
			}

			return attachmentContent;
		} catch (Exception e) {
			throw new ProviderException(e);
		} finally {
			if (transaction != null && !transaction.isClosed()) {
				transaction.close();
			}
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public AttachmentContent getAttachmentContent(WikiPage page, String name, int... version) throws ProviderException {
		if (page == null) {
			return null;
		}
		String pageId = page.getId();
		log.debug("Getting attachment for pageId: " + pageId);

		PageAttachment pageAttachment = null;
		for (PageAttachment attachItem : page.getAttachments()) {
			if (attachItem.getName().equals(name)) {
				pageAttachment = attachItem;
				break;
			}
		}

		if (pageAttachment == null) {
			log.debug("Attachment \"" + name + "\"not found - thus no attachment can exist.");
			return null;
		}

		AttachmentContent attachmentContent;
		if (version.length == 0 || version[0] == IWikiConstants.LATEST_VERSION) {
			attachmentContent = pageAttachment.forLastContent();
		} else {
			attachmentContent = pageAttachment.forVersionContent(version[0]);
		}

		return attachmentContent;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public List<AttachmentContent> getVersionHistory(final PageAttachment att) {
		final ArrayList<AttachmentContent> list = new ArrayList<>(att.getAttachContents());

		return list;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void deleteVersion(final PageAttachment att) throws ProviderException {
		// FIXME: Does nothing yet.
	}

	@Override
	public PageAttachment getAttachmentById(String attachmentId) throws ProviderException {
		if (attachmentId == null) {
			return null;
		}

		CDOTransaction transaction = null;
		try {
			transaction = storageCdo.getTransactionCDO();
			CDOQuery query;
			EClass eClassPageAttachment = Elwiki_dataPackage.eINSTANCE.getPageAttachment();
			query = transaction.createQuery("ocl",
					"PageAttachment.allInstances()->select(p:PageAttachment|p.id='" + attachmentId + "')",
					eClassPageAttachment, false);
			query.setParameter("cdoLazyExtents", Boolean.FALSE);

			PageAttachment pageAttachment = null;
			List<PageAttachment> attachments = query.getResult();
			if (attachments.size() > 0) {
				pageAttachment = attachments.get(0);
				CDOView view = storageCdo.getView();
				pageAttachment = (PageAttachment) view.getObject(pageAttachment.cdoID());
			}

			return pageAttachment;
		} catch (Exception e) {
			String msg = e.getMessage();
			if (msg.contains("Unrecognized variable")) {
				/* :FVK: workaround, when nothig any attachments exists.
				 required - check before request - how much attachments exists.*/
				return null;
			}
			throw new ProviderException(e);
		} finally {
			if (transaction != null && !transaction.isClosed()) {
				transaction.close();
			}
		}
	}

	/** {@inheritDoc} */
	@Override
	public void deleteAttachmentContent(AttachmentContent atchContent0) throws ProviderException {
		if (atchContent0 == null) {
			return;
		}

		CDOTransaction transaction = null;
		try {
			// Получить полное имя файла прикрепления.
			File dir = findAttachmentDir(atchContent0);
			File file = new File(dir, atchContent0.getPlace());

			transaction = storageCdo.getTransactionCDO();
			AttachmentContent atchContent = transaction.getObject(atchContent0);
			PageAttachment pageAtch = atchContent.getPageAttachment();

			EList<AttachmentContent> listAtchContents = pageAtch.getAttachContents();
			listAtchContents.remove(atchContent);
			if (listAtchContents.size() == 0) {
				// удалить и PageAttachment, т.к. нет AttachmentContent.
				pageAtch.getWikipage().getAttachments().remove(pageAtch);
			}

			transaction.commit();

			/* Удаление файла. */
			file.delete();
		} catch (Exception e) {
			throw new ProviderException(e);
		} finally {
			if (!transaction.isClosed()) {
				transaction.close();
			}
		}
	}

	@Override
	public boolean deleteAttachmentById(String attachmentId) throws ProviderException {
		PageAttachment att = getAttachmentById(attachmentId);
		return deleteAttachment(att);
	}

	@Override
	public boolean deleteAttachment(PageAttachment attachment0) throws ProviderException {
		if (attachment0 == null) {
			return false;
		}
		CDOTransaction transaction = null;
		try {
			List<String> allFiles = new ArrayList<>();
			transaction = storageCdo.getTransactionCDO();
			PageAttachment attachment = transaction.getObject(attachment0);

			WikiPage wikiPage = attachment.getWikipage();
			if (wikiPage != null) {
				wikiPage.getAttachments().remove(attachment);
			}

			/* Clear attachment contents list. */
			List<String> atchFiles = attachment.getAttachContents().stream().map(e -> e.getPlace())
					.collect(Collectors.toList());
			allFiles.addAll(atchFiles);
			attachment.getAttachContents().clear();

			transaction.commit();

			/* Удаление файлов. */
			File dir = findAttachmentDir(null); // :FVK: NULL - workaround.
			for (String fileName : allFiles) {
				File file = new File(dir, fileName);
				try {
					file.delete();
				} catch (Exception e1) {
				}
			}
		} catch (Exception e) {
			throw new ProviderException(e);
		} finally {
			if (!transaction.isClosed()) {
				transaction.close();
			}
		}

		return true;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void moveAttachmentsForPage(final String oldParent, final String newParent) throws ProviderException {
		log.debug("Trying to move all attachments from " + oldParent + " to " + newParent);
		/*
		* FIXME: :FVK: Add into CDO manager - move Attachments from one Page to another..
		*/
	}

}
